import {TypeCompiler} from '@sinclair/typebox/compiler'
import {Static, TObject} from '@sinclair/typebox'
import {Value} from '@sinclair/typebox/value'
import {UAParser} from 'ua-parser-js'
import {format} from 'date-fns'
import {isEmpty, isUndefined, omit} from 'lodash'
import {TokenInfo} from '../types/auth'
import {RESCODE} from './auth'
import {ipExpire, mdutcg, sensitiveProp} from './const'
import {ElysiaWS} from 'elysia/dist/ws'
import {appendFileSync, mkdirSync, existsSync, closeSync, openSync, renameSync} from 'node:fs'
import picomatch from 'picomatch'
import {DB} from './db'
import {sysIp, sysLoginLog, sysReqLog} from '../schema/schema'
import {eq, sql, desc, SQL} from 'drizzle-orm'
import RedisClient from './redis'

/** 环境变量插件 */
export const envPlugin = <T extends TObject>(env: T): Static<T> => {
  const variablesMap = Object.keys(env.properties).reduce((obj, cur) => ({...obj, [cur]: cur}), {})

  const variablesObject = Object.keys(Bun.env).reduce((carry, variableName) => {
    return variablesMap[variableName as keyof typeof variablesMap] ? {...carry, [variableName]: Bun.env[variableName]} : carry
  }, {})

  const variablesValue = Value.Default(env, variablesObject)

  const compiler = TypeCompiler.Compile(env)
  if (!compiler.Check(variablesValue)) {
    console.error('Your .env file is invalid', [...compiler.Errors(variablesValue)])
    process.exit()
  }

  return variablesObject
}

/** IP地址校验 */
export class IPValidation {
  static validateIP(refererWhiteList: string[], ip: string): boolean {
    const _ip = this.generateIP(ip)
    return (
      refererWhiteList.includes(_ip) ||
      (refererWhiteList.filter(e => e.includes('*')).length > 0 &&
        refererWhiteList
          .filter(e => e.includes('*'))
          .map(e => e.split('*')[0])
          .every(e => _ip.startsWith(e)))
    )
  }
  static generateIP(ip: string): string {
    // Handle IPv6-mapped IPv4 addresses
    if (ip.startsWith('::ffff:')) {
      ip = ip.split('::ffff:')[1]
    }
    // Handle localhost
    if (ip === '::1') {
      return 'localhost'
    }
    return ip
  }
}

/** 格式化接口请求时长 */
export const formatDuration = (start: bigint) => {
  const end = process.hrtime.bigint()
  const nanoseconds = Number(end - start)

  const durationInMicroseconds = (nanoseconds / 1e3).toFixed(0) // Convert to microseconds
  const durationInMilliseconds = (nanoseconds / 1e6).toFixed(0) // Convert to milliseconds
  let timeMessage: string = ''
  if (nanoseconds >= 1e9) {
    const seconds = (nanoseconds / 1e9).toFixed(2)
    timeMessage = `${seconds}s`
  } else if (nanoseconds >= 1e6) {
    timeMessage = `${durationInMilliseconds}ms`
  } else if (nanoseconds >= 1e3) {
    timeMessage = `${durationInMicroseconds}µs`
  } else {
    timeMessage = `${nanoseconds}ns`
  }
  return {
    timeMessage,
    time: Number(durationInMilliseconds),
  }
}

enum Color {
  GET = '32',
  POST = '33',
  PATCH = '34',
  DELETE = '31',
}
/** 格式化控制台日志样式 */
export const formatLog = async ({
  timestamp,
  ip,
  start,
  request,
  params,
  path,
  body,
  query,
  errorMsg,
  db,
  redis,
  curUser,
  code,
}: {
  timestamp: Date
  ip: string
  start: bigint
  request: Request
  path: string
  params: any
  body: any
  query: any
  db: DB
  redis: RedisClient
  errorMsg?: string
  curUser?: TokenInfo
  code?: RESCODE
}) => {
  const _method = request.method as keyof typeof Color
  const {browser, os} = UAParser(request.headers.get('user-agent') as string)
  let logStr = ''
  console.log(
    `\x1B[${Color[_method] || '90'}m\x1B[1m\u279C\x1B[39m [${format(timestamp, 'yyyy-MM-dd HH:mm:ss')}] \x1B[1m${ip}\x1B[0m\x1B[${
      Color[_method] || '90'
    }m\x1B[1m ${`[${_method}]`.padEnd(4, ' ')}\x1B[39m \x1B[3m${path || ''}\x1B[0m ${
      browser.name ? browser.name + browser.version : request.headers.get('user-agent')
    }${os.name ? ' ' + os.name + os.version : ''}`,
  )
  logStr += `[${format(timestamp, 'yyyy-MM-dd HH:mm:ss')}] ${ip} [${_method}] ${path || ''} ${
    browser.name ? browser.name + browser.version : request.headers.get('user-agent')
  }${os.name ? ' ' + os.name + os.version : ''}`
  if (!isEmpty(params)) {
    console.log(`\x1B[35m${`  Params    \u279C`.padEnd(14, ' ')}\x1B[0m \x1B[1m${JSON.stringify(params) || ''}\x1B[0m`)
    logStr += ` Params:${JSON.stringify(params) || ''}`
  }
  if (!isEmpty(query)) {
    console.log(`\x1B[35m${`  Query     \u279C`.padEnd(14, ' ')}\x1B[0m \x1B[1m${JSON.stringify(query) || ''}\x1B[0m`)
    logStr += ` Query:${JSON.stringify(query) || ''}`
  }
  if (!isEmpty(body) && path !== '/api/upload') {
    console.log(`\x1B[35m${`  Body      \u279C`.padEnd(14, ' ')}\x1B[0m \x1B[1m${JSON.stringify(hideSensitive(body)) || ''}\x1B[0m`)
    logStr += ` Body:${JSON.stringify(hideSensitive(body)) || ''}`
  }
  console.log(`\x1B[${errorMsg ? '31' : '32'}m${`${errorMsg ? '  Error     ' : '  Response  '}\u279C`.padEnd(14, ' ')} \x1B[1m${errorMsg || 'SUCCESS'} \x1B[0m`)
  logStr += ` ${errorMsg || 'Success'}`
  const consumeTime = formatDuration(start)
  console.log(`${`  UsedTime  \u279C`.padEnd(14, ' ')}\x1B[36m\x1B[1m ${consumeTime.timeMessage} ✔ \x1B[0m`)
  logStr += ` [${consumeTime.timeMessage}]\n`
  writeLogFile(logStr)

  const _params = [
    {type: 'body', value: hideSensitive(body)},
    {type: 'params', value: hideSensitive(params)},
    {type: 'query', value: hideSensitive(query)},
  ].find(({value}) => typeof value === 'object' && Object.keys(value).length > 0)

  const {prov, city, district, desc} = await ipAddress(db, ip, redis)
  // 请求日志写入
  await db.insert(sysReqLog).values({
    userId: curUser?.userId,
    ip,
    address: prov ? prov + (city ? `/${city}` : '') + (district ? `/${district}` : '') : desc,
    params: _params ? JSON.stringify(_params.value) : undefined,
    paramsType: _params?.type,
    url: path,
    method: _method,
    status: code ? code : 200,
    consumeTime: consumeTime.time,
  })
  if (path === '/api/login' && errorMsg) {
    await addLoginLog(db, {...(await getLoginLog(db, request.headers.get('user-agent') as string, ip, redis, errorMsg)), online: 0})
  }
}

/** 格式化错误信息 */
export const formatErrorMsg = (errMsg: string, path?: string) => {
  if (errMsg.includes('validation')) {
    const error = JSON.parse(errMsg)
    return `${(error.property as string).replace('/', '').replaceAll('/', '.')} => ${error.message}`
  }
  if (errMsg === 'NOT_FOUND' && path !== '/api/file') {
    if (path === '/api/file') {
      return '文件不存在'
    }
    return `接口 ${path} 不存在`
  }
  return errMsg
}

/**
 * 格式化路由
 */
export const formatPathForAuth = (path: string) => {
  const _lastIndex = path.lastIndexOf('/')
  if (!isNaN(Number(path.substring(_lastIndex + 1, path.length)))) return path.substring(0, _lastIndex).slice(1).replace(/\//g, '_')
  return path.slice(1).replace(/\//g, '_')
}

type IPAddr = {
  prov: string
  city: string
  district: string
  desc: string
}

/**
 * 根据IP获取地理位置
 */
export const ipAddress = (db: DB, ip: string, redis: RedisClient): Promise<IPAddr> => {
  return new Promise((resolve, reject) => {
    redis.get(`user:ip:${ip}`).then(async res => {
      if (res) {
        const {prov, city, district, desc} = JSON.parse(res)
        // 1、在数据库中有匹配的ip
        // 比较ip的更新时间与当前时间的差值，如果大于一周则调用接口获取最新ip详情、上传到数据库并返回最新ip的信息，如果小于一周就直接返回当前ip的信息
        resolve({prov, city, district, desc})
      } else {
        // 2、差值大于一周或者数据库未匹配到ip,调用接口获取
        resolve(await getIpInfoBy3Party(db, ip, redis))
      }
    })

    // db.query.sysIp.findFirst({where: eq(sysIp.ip, ip)}).then(async res => {
    //   if (res) {
    //     // 1、在数据库中有匹配的ip
    //     // 比较ip的更新时间与当前时间的差值，如果大于一周则调用接口获取最新ip详情、上传到数据库并返回最新ip的信息，如果小于一周就直接返回当前ip的信息
    //     const dateDiff = differenceInDays(new Date(), new Date(res.updateTime as string))
    //     if (dateDiff <= 7) {
    //       // 1.1、差值小于一周
    //       resolve({prov: res.prov as string, city: res.city as string, district: res.district as string, desc: res.desc})
    //     } else {
    //       // 1.2、差值大于一周
    //       resolve(await getIpInfoBy3Party(db, ip))
    //     }
    //   } else {
    //     // 2、数据库未匹配到ip,调用接口获取
    //     resolve(await getIpInfoBy3Party(db, ip))
    //   }
    // })
  })
}

export const getIpInfoBy3Party = (db: DB, ip: string, redis: RedisClient): Promise<IPAddr> => {
  return new Promise((resolve, reject) => {
    // IP数据云
    fetch(`https://api.ipdatacloud.com/v3/query_jsonp?ip=${ip}&key=de47906d4c5211ee98ee00163e25360e`, {method: 'GET'})
      .then(res => res.json())
      .then(async res => {
        const _detail = res.data.location
        const _res = {
          prov: _detail.province === '保留' ? '' : (_detail.province as string),
          city: _detail.province === '保留' || mdutcg.includes(_detail.province) ? '' : (_detail.city as string),
          district: _detail.district as string,
          street: _detail.street as string,
          desc: _detail.province === '保留' ? '内部IP' : '',
        }
        const params = {
          ..._res,
          country: _detail.province === '保留' ? '' : (_detail.country as string),
          lng: _detail.longitude as string,
          lat: _detail.latitude as string,
          isp: _detail.isp as string,
          zipcode: _detail.zip_code as string,
        }
        await db
          .insert(sysIp)
          .values({ip, ...params})
          .onDuplicateKeyUpdate({set: {...params}})
        await redis.set(`user:ip:${ip}`, JSON.stringify(omit(_res, ['district'])), ipExpire)
        resolve(_res)
      })
    // // 腾讯云
    // fetch(`https://apis.map.qq.com/ws/location/v1/ip?ip=${ip}&key=RMJBZ-2IP64-CSNUJ-DLU7O-O3DF7-MMB5E`, {method: 'GET'})
    //   .then(res => res.json())
    //   .then(async res => {
    //     const _detail = res.result.ad_info
    //     const _location = res.result.location
    //     const _res = {
    //       prov: res.status === 375 ? '' : _detail.province.replace(/省|市/g, ''),
    //       city: res.status === 375 || mdutcg.includes(_detail.province.replace(/省|市/g, '')) ? '' : (_detail.city.replace(/市/g, '') as string),
    //       district: _detail.district as string,
    //       street: '',
    //       desc: res.status === 375 ? '内部IP' : '',
    //     }
    //     const params = {
    //       ..._res,
    //       country: res.status === 375 ? '' : (_detail.nation as string), // 国家
    //       lng: _location.lng as string,
    //       lat: _location.lat as string,
    //       isp: _detail.isp as string,
    //       zipcode: _detail.zip_code as string,
    //     }
    //   })
    // 埃文科技
    // https.get(`https://token.ip.api.useragentinfo.com/json?token=ab28a017dc0b7536f452fd951aed51d2&ip=${ip}`, res => {
    //   let chunks: any[] = []
    //   res.on('data', chunk => chunks.push(chunk))
    //   res.on('end', async () => {
    //     const json = iconv.decode(Buffer.concat(chunks), 'utf-8')
    //     const _detail = JSON.parse(json)
    //     const _res = {
    //       prov: _detail.province as string,
    //       city: _detail.city as string,
    //       district: _detail.area as string,
    //       desc: _detail.province === '' ? '内部IP' : '',
    //     }
    //     const params = {
    //       ..._res,
    //       country: _detail.province === '' ? '' : (_detail.country as string),
    //       isp: _detail.isp as string,
    //     }
    //     await prisma.ip.upsert({
    //       create: {ip, ...params},
    //       update: {...params},
    //       where: {ip},
    //     })
    //     resolve(_res)
    //   })
    // })
    // 太平洋网络
    // http.get(`http://whois.pconline.com.cn/ipJson.jsp?json=true&ip=${ip || ''}`, res => {
    //   let chunks: any[] = []
    //   res.on('data', chunk => {
    //     chunks.push(chunk)
    //   })
    //   res.on('end', () => {
    //     const json = iconv.decode(Buffer.concat(chunks), 'gbk')
    //     resolve(JSON.parse(json))
    //   })
    // })
  })
}

export const hideSensitive = (params: Object) => {
  if (!params || Object.keys(params).length === 0) return {}
  return Object.fromEntries(
    Object.entries(params).map(val => {
      sensitiveProp.includes(val[0]) && (val[1] = '*******')
      return val
    }),
  )
}

export const getLoginLog = async (db: DB, userAgent: string, ip: string, redis: RedisClient, msg?: string) => {
  const {prov, city, district, desc} = await ipAddress(db, ip, redis)
  const {browser, os} = UAParser(userAgent)
  return {
    ip,
    address: prov ? prov + (city ? `/${city}` : '') + (district ? `/${district}` : '') : desc,
    browser: browser.name ? browser.name + browser.version : undefined,
    os: os.name ? os.name + os.version : undefined,
    status: 0,
    reason: msg,
  }
}

/**
 * 新增登录日志
 */
export const addLoginLog = async (
  db: DB,
  params: {userId?: number; ip?: string; address?: string; browser?: string; os?: string; status: number; reason?: string; online: number},
) => {
  await db.insert(sysLoginLog).values(params)
}

/**
 * 修改登录日志
 */
export const updLoginLog = async (db: DB, params: {userId: number; online: number}) => {
  await db.update(sysLoginLog).set({online: params.online}).where(eq(sysLoginLog.userId, params.userId)).orderBy(desc(sysLoginLog.createTime)).limit(1)
}

/** 获取websocket连接的IP池 */
export const getIPPool = (pool: Map<string, ElysiaWS<any>>) => {
  const res: string[] = []
  pool.forEach(ws => {
    res.push((ws.data.store as any).ip + '[' + ws.id + ']')
  })
  return res
}

export const writeLogFile = (msg: string) => {
  if (!existsSync('logs')) {
    mkdirSync('logs')
  }
  const path = `logs/log_${format(new Date(), 'yyyyMMdd')}`
  appendFileSync(`${path}.log`, msg, {encoding: 'utf-8'})
  const file = Bun.file(`${path}.log`)
  if (file.size > 1024 * 1024) {
    let count = 1
    while (existsSync(`${path}_${count}.log`)) {
      count++
    }
    renameSync(`${path}.log`, `${path}_${count}.log`)
    closeSync(openSync(`${path}.log`, 'w'))
  }
}

const parseUrl = (url: string, prop?: keyof URL) => {
  try {
    const parsed = new URL(url)
    return (prop ? parsed[prop] : parsed) as string
  } catch (err) {
    return null
  }
}

const isSafeDomain = (domain: string, whiteList: string[]) => {
  domain = domain.toLowerCase()
  const hostname = '.' + domain
  return whiteList.some(rule => {
    // Check whether we've got '*' as a wild character symbol()
    if (rule.includes('*')) return picomatch(rule)(domain)
    if (domain === rule) return true
    if (!/^\./.test(rule)) {
      rule = `.${rule}`
    }
    return hostname.endsWith(rule)
  })
}

/** 判断referer是否合法 */
export const checkReferer = (request: Request, refererWhiteList: string[]) => {
  if (request.url.endsWith('/ws')) return
  const _referer = (request.headers.get('Referer') || '').toLowerCase()
  if (!_referer) return 'missing csrf referer'
  const _host = parseUrl(_referer, 'host')
  if (!_host || !isSafeDomain(_host, refererWhiteList)) return 'invalid csrf referer'
}

// 格式化sql条件
export const toSql = <T>(condition: T | undefined, cb: (args: T) => SQL) => {
  return !isUndefined(condition) ? cb(condition) : undefined
}
